# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilityFindingSignaturesForFindings, :migration, schema: 20220326161803 do
  let(:namespaces) { table(:namespaces) }
  let(:group) { namespaces.create!(name: 'foo', path: 'foo') }
  let(:projects) { table(:projects) }
  let(:findings) { table(:vulnerability_occurrences) }
  let(:finding_signatures) { table(:vulnerability_finding_signatures) }
  let(:scanners) { table(:vulnerability_scanners) }
  let(:identifiers) { table(:vulnerability_identifiers) }

  let!(:project) { projects.create!(namespace_id: group.id, name: 'gitlab', path: 'gitlab') }

  let!(:scanner) do
    scanners.create!(project_id: project.id, external_id: 'semgrep', name: 'Semgrep')
  end

  let!(:identifier) do
    identifiers.create!(project_id: project.id, fingerprint: SecureRandom.hex(20), external_type: 'semgrep_rule_id', external_id: '42', name: '42')
  end

  let(:raw_tracking_value) do
    raw_tracking.dig(:tracking, :items).first.fetch(:signatures).first.fetch(:value)
  end

  it 'updates finding signatures' do
    finding1 = findings.create!(finding_params)
    signature1 = finding_signatures.create!(
      finding_id: finding1.id,
      algorithm_type: 'scope_offset',
      signature_sha: Digest::SHA1.digest(raw_tracking_value)
    )

    finding2 = findings.create!(finding_params)
    # Generate signature with SHA not matching `raw_metadata`
    signature2 = finding_signatures.create!(
      finding_id: finding2.id,
      algorithm_type: 'scope_offset',
      signature_sha: Digest::SHA1.digest("foo/bar.rb|Something[0]|else[0]:1")
    )

    service = described_class.new
    logger = ::Gitlab::BackgroundMigration::Logger.build
    service.instance_variable_set(:@logger, logger)

    expect(logger).not_to receive(:error)
    expect do
      service.perform(signature1.id, signature2.id)
    end.to change { finding_signatures.count }.by(0)

    expect(
      finding_signatures.find_by(finding_id: finding1.id).signature_sha
    ).to eq Digest::SHA1.digest(raw_tracking_value)

    expect(
      finding_signatures.find_by(finding_id: finding2.id).signature_sha
    ).to eq Digest::SHA1.digest(raw_tracking_value)
  end

  it 'logs error on unexpected failure' do
    finding = findings.create!(
      finding_params({}) # empty tracking info
    )
    signature = finding_signatures.create!(
      finding_id: finding.id,
      algorithm_type: 'scope_offset',
      signature_sha: Digest::SHA1.digest(raw_tracking_value)
    )

    service = described_class.new

    allow(ApplicationRecord)
      .to receive(:legacy_bulk_insert)
      .and_raise(ActiveRecord::RecordInvalid)

    expect_next_instance_of(::Gitlab::BackgroundMigration::Logger) do |logger|
      expect(logger).to receive(:error).once
    end
    expect do
      service.perform(signature.id, signature.id)
    end.to change { finding_signatures.count }.by(-1)
  end

  it 'logs error on malformed JSON failure' do
    params = finding_params({})
    params[:raw_metadata] = '{' # malformed JSON

    finding = findings.create!(params)
    signature = finding_signatures.create!(
      finding_id: finding.id,
      algorithm_type: 'scope_offset',
      signature_sha: Digest::SHA1.digest(raw_tracking_value)
    )

    service = described_class.new

    expect_next_instance_of(::Gitlab::BackgroundMigration::Logger) do |logger|
      expect(logger).to receive(:error).once
    end
    expect do
      service.perform(signature.id, signature.id)
    end.to change { finding_signatures.count }.by(-1)
  end

  it 'drops invalid row when metadata is missing tracking' do
    finding = findings.create!(
      finding_params({}) # empty tracking info
    )
    signature = finding_signatures.create!(
      finding_id: finding.id,
      algorithm_type: 'scope_offset',
      signature_sha: Digest::SHA1.digest(raw_tracking_value)
    )

    service = described_class.new

    expect_next_instance_of(::Gitlab::BackgroundMigration::Logger).never
    expect do
      service.perform(signature.id, signature.id)
    end.to change { finding_signatures.count }.by(-1)
  end

  it 'drops invalid row when tracking signatures data is malformed' do
    finding = findings.create!(
      finding_params({ "tracking": { "itemssss": [] } }) # malformed tracking info
    )
    signature = finding_signatures.create!(
      finding_id: finding.id,
      algorithm_type: 'scope_offset',
      signature_sha: Digest::SHA1.digest(raw_tracking_value)
    )

    service = described_class.new

    expect_next_instance_of(::Gitlab::BackgroundMigration::Logger).never
    expect do
      service.perform(signature.id, signature.id)
    end.to change { finding_signatures.count }.by(-1)
  end

  def finding_params(tracking_details = raw_tracking)
    uuid = SecureRandom.uuid

    {
      severity: Enums::Vulnerability::SEVERITY_LEVELS[:medium],
      confidence: Enums::Vulnerability::CONFIDENCE_LEVELS[:medium],
      report_type: Enums::Vulnerability::REPORT_TYPES[:sast],
      project_id: project.id,
      scanner_id: scanner.id,
      primary_identifier_id: identifier.id,
      project_fingerprint: SecureRandom.hex(20),
      location_fingerprint: Digest::SHA1.hexdigest(SecureRandom.hex(10)),
      uuid: uuid,
      name: "Vulnerability Finding #{uuid}",
      metadata_version: '14.0.0',
      raw_metadata: Gitlab::Json.dump(raw_metadata.merge(tracking_details))
    }
  end

  def raw_metadata
    {
      "id": "756a4302f62d4b44d8d64e1a925d7a076fcc80918b7319e62bb28d4d4baa2bc8",
      "category": "sast",
      "name": "Possible unprotected redirect",
      "message": "Possible unprotected redirect",
      "cve": "373414e0effe673bb93d1d8994f3e511ff089ce79337a16577e087556e9ae3cd",
      "severity": "Low",
      "confidence": "Low",
      "scanner": { "id": "brakeman", "name": "Brakeman" },
      "location": { "file": "app/controllers/groups_controller.rb", "start_line": 6, "class": "GroupsController", "method": "new_group" },
      "identifiers": [{ "type": "brakeman_warning_code", "name": "Brakeman Warning Code 18", "value": "18", "url": "https://brakemanscanner.org/docs/warning_types/redirect/" }]
    }
  end

  def raw_tracking(file = "app/controllers/groups_controller.rb")
    { "tracking": { "type": "source", "items": [{ "file": file, "line_start": 6, "line_end": 6, "signatures": [{ "algorithm": "scope_offset", "value": "#{file}|GroupsController[0]|new_group[0]:4" }] }] } }
  end
end
